UserProfileHeader.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. 'use client';
  2. import { useState } from 'react';
  3. import Link from 'next/link';
  4. import { BadgeCheck, Shield, MoreHorizontal, Pencil, Flag, Link as LinkIcon } from 'lucide-react';
  5. import {
  6. DropdownMenu,
  7. DropdownMenuContent,
  8. DropdownMenuItem,
  9. DropdownMenuTrigger
  10. } from '@/components/ui/dropdown-menu';
  11. import FollowButton from '@/app/component/FollowButton';
  12. import NoteButton from '@/app/(main)/note/_components/NoteButton';
  13. import { UserProfileDto } from '@/types/account/profile';
  14. import { buildChannelUrl, formatHandle } from '@/lib/utils/channel';
  15. import UserProfileEditDialog from './UserProfileEditDialog';
  16. type Props = {
  17. profile: UserProfileDto;
  18. };
  19. function formatCount(n: number): string {
  20. if (n >= 10000) return `${(n / 10000).toFixed(n >= 100000 ? 0 : 1)}만`;
  21. if (n >= 1000) return `${(n / 1000).toFixed(1)}천`;
  22. return n.toLocaleString();
  23. }
  24. export default function UserProfileHeader({ profile }: Props)
  25. {
  26. const [editOpen, setEditOpen] = useState(false);
  27. const displayName = profile.name || profile.memberSID;
  28. const avatarInitial = (displayName.charAt(0) || '?').toUpperCase();
  29. const handle = profile.channelHandle ? formatHandle(profile.channelHandle) : `@${profile.memberSID}`;
  30. const handleShare = async () =>
  31. {
  32. const url = `${window.location.origin}/user/${profile.memberSID}`;
  33. try {
  34. await navigator.clipboard.writeText(url);
  35. alert('프로필 링크가 복사되었습니다.');
  36. } catch {
  37. prompt('링크를 복사하세요:', url);
  38. }
  39. };
  40. return (
  41. <header className="user-profile__header">
  42. <div className="user-profile__banner">
  43. {profile.bannerUrl ? (
  44. <img src={profile.bannerUrl} alt={`${displayName} 배너`} />
  45. ) : (
  46. <div className="user-profile__banner-placeholder" />
  47. )}
  48. </div>
  49. <div className="user-profile__identity">
  50. <div className="user-profile__avatar-wrap">
  51. {profile.thumb ? (
  52. <img src={profile.thumb} alt={displayName} className="user-profile__avatar" />
  53. ) : (
  54. <span className="user-profile__avatar">
  55. <span className="user-profile__avatar-fallback">{avatarInitial}</span>
  56. </span>
  57. )}
  58. </div>
  59. <div className="user-profile__actions">
  60. {profile.isSelf ? (
  61. <>
  62. <button type="button" className="user-profile__edit-btn" onClick={() => setEditOpen(true)}>
  63. <Pencil size={14} /> 편집
  64. </button>
  65. <UserProfileEditDialog
  66. open={editOpen}
  67. onOpenChange={setEditOpen}
  68. profile={profile}
  69. />
  70. </>
  71. ) : (
  72. <>
  73. <FollowButton memberSID={profile.memberSID} initialFollowing={profile.isFollowing} />
  74. <NoteButton
  75. target={{ memberID: profile.memberID, displayName: displayName, thumbnailUrl: profile.thumb }}
  76. className="user-profile__note-btn"
  77. />
  78. </>
  79. )}
  80. <DropdownMenu>
  81. <DropdownMenuTrigger asChild>
  82. <button type="button" className="user-profile__more-btn" aria-label="더보기">
  83. <MoreHorizontal size={18} />
  84. </button>
  85. </DropdownMenuTrigger>
  86. <DropdownMenuContent align="end">
  87. <DropdownMenuItem onClick={handleShare}>
  88. <LinkIcon size={14} className="mr-2" /> 프로필 주소 복사
  89. </DropdownMenuItem>
  90. {!profile.isSelf && (
  91. <DropdownMenuItem>
  92. <Flag size={14} className="mr-2" /> 신고하기
  93. </DropdownMenuItem>
  94. )}
  95. </DropdownMenuContent>
  96. </DropdownMenu>
  97. </div>
  98. </div>
  99. <div className="user-profile__info">
  100. <h1 className="user-profile__name">
  101. {displayName}
  102. {profile.isCreator && (
  103. <span className="user-profile__badge user-profile__badge--creator" title="크리에이터">
  104. <BadgeCheck size={18} fill="currentColor" stroke="#fff" strokeWidth={2.5} />
  105. </span>
  106. )}
  107. {profile.isAdmin && (
  108. <span className="user-profile__badge user-profile__badge--admin" title="관리자">
  109. <Shield size={16} />
  110. </span>
  111. )}
  112. </h1>
  113. <p className="user-profile__handle">
  114. <span className="user-profile__handle-text">
  115. {profile.channelSID ? (
  116. <Link
  117. href={buildChannelUrl({ handle: profile.channelHandle, channelSID: profile.channelSID })}
  118. className="user-profile__handle-link"
  119. aria-label={`${handle} 크리에이터 채널로 이동`}
  120. >
  121. {handle}
  122. </Link>
  123. ) : (
  124. handle
  125. )}
  126. </span>
  127. </p>
  128. <p className="user-profile__counts">
  129. <Link href={`/user/${profile.memberSID}/followers`} className="user-profile__counts-link">
  130. <strong>{formatCount(profile.followerCount)}</strong> 팔로워
  131. </Link>
  132. <Link href={`/user/${profile.memberSID}/following`} className="user-profile__counts-link">
  133. <strong>{formatCount(profile.followingCount)}</strong> 팔로우
  134. </Link>
  135. <Link href={`/user/${profile.memberSID}`} className="user-profile__counts-link" aria-label={`게시물 ${profile.postCount + profile.feedPostCount}개 보기`}>
  136. <strong>{formatCount(profile.postCount + profile.feedPostCount)}</strong> 게시물
  137. </Link>
  138. </p>
  139. {profile.intro && (
  140. <div className="user-profile__intro" dangerouslySetInnerHTML={{ __html: profile.intro }} />
  141. )}
  142. </div>
  143. </header>
  144. );
  145. }